// ==UserScript== // @name [STEAM] 创意工坊 - 优化 // @name:en [STEAM] Workshop - optimization // @description 主要功能:1.美化部分界面 2.翻译模组简介 3.翻译评论区 4.备份订阅 // @description:en Major Abilities:1. Beautification partial interface 2.Introduction to the translation model 3. Translation and discussion section // @namespace Violentmonkey Scripts // @match *://steamcommunity.com/* // @license GPLv3 // @version 2.6 // @author Mnaisuka // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @connect translate.google.com // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js // ==/UserScript== (async function () { 'use strict'; await SetLanguage() await SetTranslAPI(); var Lang = { "ui": GM_getValue('language', "zh"),//中文 = zh | english = en | 日本語 = jp "en": { "页面类型": "page type", "翻译评论": "Translate comments", "翻译失败": "Translation failed", "错误代码": "error code", "原文与译文并行显示": "The original and translation are displayed in parallel", "正在翻译": "Being translated", "网络超时": "network timeout", "未知错误": "unknown mistake", "备份全部订阅": "Backup ALL", "还原备份订阅": "Restore Backup" }, "jp": {//不再更新中英以外的语言 "页面类型": "ページタイプ", "翻译评论": "コメントを翻訳", "翻译失败": "翻訳に失敗しました", "错误代码": "エラーコード", "原文与译文并行显示": "原文と訳文が並列で表示されます", "正在翻译": "翻訳中", "网络超时": "ネットワークタイムアウト", "未知错误": "未知の間違い", } }; var config = { bilingual: GM_getValue("bilingual", true) }; await SetConfigMenu() const init = { load: function () { var link = window.location.href var rules = this.rule for (keyNmae in rules) { var focus = rules[keyNmae] var result = RegExp(focus[0], "i").exec(link) if (result != null) { console.log("页面类型", keyNmae, " | ", "使用函数", focus[1]); for (let i = 0; i < focus[1].length; i++) { focus[1][i].call(null, { result: result.slice(1,), rules: focus[0] }) } } } }, rule: { "browse-items": [".*://steamcommunity.com/workshop/browse/.*&browsesort=.*", [browse_items_translation]],//创意工坊 - 浏览 "filedetails": [".*://steamcommunity.com/.*/filedetails/.*", [transl_thread, transl_tntroduction]],//模组 - 简介 "mysubscriptions": [".*://steamcommunity.com/id/.*/myworkshopfiles/\\?(?=.*appid=\\d+)(?=.*&browsefilter=mysubscriptions)", [back_mysubscriptions]], "workshop-all": [".*://steamcommunity.com/.*", [TranslSuspensionPrompt]], "mysubscriptions": [".*://steamcommunity.com/id/.*/(?=.*browsefilter=mysubscriptions)", [TranslMysubscriptions]], } }; (function () { init.load() }()) //---------------------------- async function browse_items_translation() {//浏览 - 项目 - 样式\美化 var background_color = "rgb(27 40 56)" //-------------------------------- var css = { ".workshopBrowseItems": ["grid-template-columns", "1fr"], ".workshopItem": ["margin-bottom", "0px"], ".workshopItem .ugc": ["float", "left"], ".workshopItem .fileRating": ["padding-left", "8px"], ".workshopItem .item_link .workshopItemTitle.ellipsis": ["padding-left", "8px", "max-width", "375px"], ".workshopItem .workshopItemAuthorName.ellipsis": ["padding-left", "8px"] } for (key in css) { for (let i = 0; i < css[key].length; i++) { $(key).css(css[key][i], css[key][i + 1]) } } var item = $(".workshopItem") for (let index = 0; index < item.length; index++) { var data = $(".workshopBrowseItems script")[index] var json = JSON.parse(/({.*})/.exec(data.textContent || data.innerText)[1]) $(item[index].children[item[index].children.length - 1]).remove(); var ele = document.createElement('a') ele.setAttribute("class", "open-text") var introduction = document.createElement('div') ele.appendChild(introduction) item[index].appendChild(ele) $(introduction).css("overflow", "hidden").css("padding-left", "8px").css("color", "#8F98A0").css("white-space", "break-spaces") $(".fileRating").css("margin-top", "0px") $(item[index]).css("border", "1px solid #000").css("padding", "2px").css("background-color", background_color) var originalText = json["description"] introduction.innerHTML = "[" + getLang("正在翻译") + "]\n" + originalText var result = await translation(htmlDecode(originalText)) var title = $(item[index]).find('.workshopItemTitle.ellipsis') var title_node = title[0] var title = title[0].innerHTML var title_transl = await translation(title) //title_node.innerHTML = config.bilingual ? title_transl.text + " | " + title : title_transl.text title_node.innerHTML = title_transl.text + " | " + title if (result.error == null) { introduction.innerHTML = result.text } else { introduction.innerHTML = result.error + htmlDecode(originalText) } } } function transl_thread() {//评论区翻译 if ($('.workshop_item_header').length == 0) { return null //合集 } var Translate_button = document.createElement('button') Translate_button.setAttribute("class", "translate_button") Translate_button.setAttribute("style", "padding: 0px 6px;color: rgb(0, 0, 0);font-size: 12px;line-height: 24px;margin-left: 2px;float: right;") Translate_button.innerHTML = getLang("翻译评论") var commentthread_count = $(".commentthread_count") if (commentthread_count.length == 0) { return null } commentthread_count[0].appendChild(Translate_button) Translate_button.onclick = async function () { var node = $(".commentthread_comment_text") for (let i = 0; i < node.length; i++) { if (node[i].getAttribute("translate")) {//避免重复翻译 reverse.call(node[i]) continue; } var result = await translation(node[i].innerHTML) if (result.error == null) { node[i].innerHTML = result.text node[i].setAttribute("translate_state", 1) node[i].setAttribute("translate_text", result.text) node[i].setAttribute("original", result.original.text) node[i].setAttribute("translate", true) node[i].onclick = reverse } else { node[i].innerHTML = result.error + htmlDecode(node[i].innerHTML) } } function reverse() { if (this.getAttribute("translate_state") == 1) { this.setAttribute("translate_state", 0) this.innerHTML = this.getAttribute("original") } else { this.setAttribute("translate_state", 1) this.innerHTML = this.getAttribute("translate_text") } } } } async function transl_tntroduction() {//翻译简介 if ($('.workshop_item_header').length == 0) { return null //合集 } var bilingual = config.bilingual //-------------------------------------- var itemTitle = $('.workshopItemTitle') var result = await translation(itemTitle.text()) if (!result.skip) { var sText = bilingual ? result.text + " | " + itemTitle.text() : result.text $('.game_area_purchase_game h1').contents().each( function () { if (this.nodeName.toLowerCase() == "#text") { this.nodeValue = sText } } ) itemTitle.text(sText) } //----------------------------------------- var merge = null var node = Array.from($('.workshopItemDescription *,.workshopItemDescription').contents()) for (let i = 0; i < node.length; i++) { var nodeName = node[i].nodeName.toLowerCase(); if (nodeName == "#text") { var value = node[i].nodeValue if (!!$.trim(value).length) { if (merge == null) { merge = `@${i}=${value}=@` } else { merge += `\n@${i}=${value}=@` } } } } var result = await translation(merge || "") if (!result.skip) { var match = new RegExp('@(\\d+)=(.*)=@', 'g') data = match.exec(result.text) for (; data != null;) { var focus = node[data[1]] var sText = bilingual ? data[2] + " | " + focus.nodeValue : data[2] focus.nodeValue = sText data = match.exec(result.text) } } } function back_mysubscriptions() {//备份订阅 $( back_init ) function back_init() { $('.menu_panel:last').append('
' + getLang('备份全部订阅') + '
') $('.menu_panel:last').append('
' + getLang('还原备份订阅') + '
') $('.btn_grey_steamui.btn_medium.back').click(back_click) $('.btn_grey_steamui.btn_medium.restore').click(restore_click) async function back_click() { var size = $('.pagelink:last').text() var Params = getUrlParams2(window.location.href) var id = Params['appid'] var numperpage = "&numperpage=" + (Params['numperpage'] || "10") var url = window.location.origin + window.location.pathname var m = Toast('共' + size + '页模组') var save = [] for (let index = 0; index < Math.trunc(size); index++) { var Curl = url + "?appid=" + id + "&browsefilter=mysubscriptions" + numperpage + "&p=" + (index + 1) var doc = await new Promise( function (load) { GM_xmlhttpRequest({ method: 'GET', url: Curl, onload: load }) } ) var local_document = stringToDocument(doc.responseText); var parent = $(local_document).find('.workshopItemPreviewHolder').parent() for (let index = 0; index < parent.length; index++) { save.push(getUrlParams2($(parent[index]).attr('href'))['id']) } m.innerHTML = getLang("正在获取第") + (index + 1) + "/" + size + "页" } m.innerHTML = "模组共有" + save.length + "个" GM_setValue("back_" + id, save) setTimeout(function () { m.remove() }, 3000) } async function restore_click() { var Params = getUrlParams2(window.location.href) var appid = Params['appid'] var back_mods = GM_getValue("back_" + appid, []) var sessionid = /sessionid=([A-Za-z0-9]*)|$/.exec(document.cookie)[1] var m = Toast('共' + back_mods.length + '个模组') if (back_mods.length == 0) { m.innerHTML = "找不到该游戏的相关备份,游戏ID=" + appid setTimeout(function () { m.remove() }, 3000) return null } var fail = 0 for (let index = 0; index < back_mods.length; index++) { var result = await new Promise( function (load) { $.ajax({ type: "POST", url: 'https://steamcommunity.com/sharedfiles/subscribe', data: "id=" + back_mods[index] + "&appid=" + appid + "&sessionid=" + sessionid, success: load, crossDomain: true, xhrFields: { withCredentials: true } }); } ) var success = result success == 2 ? fail++ : "200" console.log(fail); m.innerHTML = `ID:${back_mods[index]} - ${index + 1}/${back_mods.length} (${success == 2 ? "失败" : "成功"})` } m.innerHTML = `恢复比例${back_mods.length}/${back_mods.length - fail}` setTimeout(function () { m.remove(); window.location.reload(); }, 3000) } } function getUrlParams2(url) { let urlStr = url.split('?')[1] const urlSearchParams = new URLSearchParams(urlStr) const result = Object.fromEntries(urlSearchParams.entries()) return result } } function TranslSuspensionPrompt() { var node = $(".hover_box.shadow_content")[0] if (!node) { return null } var old = { title: "", content: "", }; (function ListenChange() { let config = { childList: true, characterData: true, subtree: true, }; const mutationCallback = (mutationsList) => { for (let mutation of mutationsList) { let type = mutation.type; if (type == "childList") { (async function () { var title = $('.hoverWorkshopItemTitle')[0].innerHTML var content = $('.hoverWorkshopItemDesc')[0].innerHTML if (old.title != title && old.content != content) { old.title = title; old.content = content old.time = Date.now(); var oid_time = old.time TranslUI("正在翻译...", "正在翻译...") var tl_title = await translation(title) var tl_content = await translation(content) if (oid_time == old.time) { if (!tl_content.skip) { TranslUI(tl_title.text, tl_content.text) } } } }()) } } }; var observe = new MutationObserver(mutationCallback); observe.observe(node, config) }()) function TranslUI(title, content) { if ($('.suspension.prompt').length == 0) { var ui = document.createElement('div') $(ui).attr('class', 'suspension prompt') $('div.hover_box.shadow_content').append(ui) } else { var ui = $('.suspension.prompt')[0] } ui.innerHTML = '
' + title + '
' + content + '
' return ui } } async function TranslMysubscriptions() { var title = $('.workshopItemSubscriptionDetails a .workshopItemTitle') for (let index = 0; index < title.length; index++) { var originalText = title[index].innerHTML var Transl = await translation(originalText) title[index].innerHTML = Transl.text + ` > ` + originalText } } //---------------------------- async function translation(text, sl = "auto", tl = Lang.ui, api) {//原文,原文语言,目标语言 if (Lang.ui == "zh") { if (Does_it_contain_ch(text)) { return { text: text, status: 200, error: gError('检测到中文 > 跳过翻译', "216,99,72"), original: { text: text, result: {} }, skip: true } } } var focus = api || GM_getValue('Transl', "sougou") var hash = hashVal(text) var offline = GM_getValue_local("local_data", hash, null) if (offline != null) { if (offline.api == focus) { //console.log("使用离线数据加载", hash, offline.api, offline.value); return offline.value } } var consult = { google: { "auto": "auto", "zh": "zh-CN", "jp": "ja", "en": "en" }, sougou: { "auto": "auto", "zh": "zh", "jp": "ja", "en": "en" } } var translapi = { sougou: Sougou, google: Google } var value = await translapi[focus](text, consult.sougou[sl], consult.sougou[tl]) if (value.status == 200) {//存储到本地 value.original.result = {} GM_setValue_local("local_data", hash, { api: focus, time: new Date() * 1, value: value }) } return value //---------------------------- async function Google(text, sl, tl) { var result = await new Promise( function (end, error) { GM_xmlhttpRequest({ method: 'GET', url: 'https://translate.google.com/m?hl=zh-CN&sl=' + sl + '&tl=' + tl + '&ie=UTF-8&prev=_m&q=' + encodeURIComponent(text), onload: end, onerror: error }) } ).catch( function (error) { return error //console.log("Google - Error", error); } ) if (result.status == 200) { var local_document = stringToDocument(result.responseText); var local_translate = $(local_document).find('.result-container'); if (local_translate.length == 0) { var error = { text: "错误代码", status: -1, translation: text }; } else { var local_result = htmlDecode(local_translate[0].innerHTML).split('\n') for (let index = 0; index < local_result.length; index++) { local_result[index] = $.trim(local_result[index]) } local_result = local_result.join('\n') } if (!error) { return { text: local_result, status: 200, error: null, original: { text: text, result: result } } } else { return { text: error.translation, status: error.status, error: gError(error.text, "247,151,103"), original: { text: text, result: result } } } } else { return { text: null, status: result.status, error: gError('错误代码', "247,151,103", " > " + (result.status == 0 ? getLang("网络超时") : getLang("未知错误") + result.status)), original: { text: text, result: result } } } } async function Sougou(text, sl, tl) { var result = await new Promise( function (end, error) { GM_xmlhttpRequest({ method: 'GET', url: `https://fanyi.sogou.com/text?fr=default&keyword=${encodeURIComponent(text)}&transfrom=${sl}&transto=${tl}&model=general&errcode=s10`, onload: end, onerror: error }) } ).catch( function (error) { return error //console.log("Sogou - Error", error); } ) if (result.status == 200) { var local_document = stringToDocument(result.responseText); var trans_result = local_document.querySelector("#trans-result") if (!trans_result) { var error = { text: "解析失败", status: -1, translation: text }; m = Toast("搜狗翻译 - 解析失败\n\nPS:已切换为谷歌翻译") setTimeout(function () { m.remove() }, 15000) GM_setValue('Transl', "google") return translation(text, sl, tl, "google") } else { var trans_result = trans_result.innerText.split('\n') for (let index = 0; index < trans_result.length; index++) { trans_result[index] = $.trim(trans_result[index]) } trans_result = trans_result.join('\n') var local_result = trans_result; } if (!error) { return { text: htmlDecode(local_result), status: 200, error: null, original: { text: text, result: result } } } else { return { text: error.translation, status: error.status, error: gError(error.text, "247,151,103"), original: { text: text, result: result } } } } else { return { text: null, status: result.status, error: gError('错误代码', "247,151,103", " = " + result.status), original: { text: text, result: result } } } } //---------------------------- function gError(text, color = '247,151,103', expansion = '') { return `
[${getLang(text) + expansion}]↵

` } } function htmlDecode(text) { var temp = document.createElement("div"); temp.innerHTML = text; var output = temp.innerText || temp.textContent; temp = null; return output; } function getLang(value) { if (Lang["ui"] == "zh") return value; return Lang[Lang["ui"]][value] || value } function stringToDocument(txt) { try //Internet Explorer { xmlDoc = new ActiveXObject("Microsoft.HTMLDOM"); xmlDoc.async = "false"; xmlDoc.loadXML(txt); return (xmlDoc); } catch (e) { try //Firefox, Mozilla, Opera, etc. { parser = new DOMParser(); xmlDoc = parser.parseFromString(txt, "text/html"); return (xmlDoc); } catch (e) { alert(e.message) } } return (null); } async function SetTranslAPI() { if (GM_getValue('Transl', null) == null) { var m = Toast("正在检索可用翻译API") return await new Promise( (success, error) => { GM_xmlhttpRequest({ method: 'GET', url: `https://translate.google.com`, onload: (res) => { GM_setValue('Transl', 'google') m.remove() m = Toast("标记Google翻译为可用") setTimeout(function () { m.remove() }, 5000) success(res) }, onerror: (res) => { GM_setValue('Transl', 'sougou') m.remove() m = Toast("标记Sougou翻译为可用") setTimeout(function () { m.remove() }, 5000) error(res) } }) } ) } console.log('TranslAPI', GM_getValue('Transl', null)); return null } async function SetLanguage() { if (!GM_getValue('language', null)) { var language = navigator.language.toLowerCase() var preset = "zh" var text = "请确认当前语种是否为中文" if (language == "ja") { var preset = "jp" var text = "現在の言語が日本語かどうかを確認してください" } else if (language == "en") { var preset = "en" var text = "Please confirm whether the current language is English" } if (confirm(text)) { GM_setValue('language', preset) } else { var pr = prompt("日本語を表示する jpを入力してください。\nDisplay English, please enter EN").toLowerCase() if (pr == "en" || pr == "jp") { GM_setValue('language', pr) } else { confirm('没有指定语言,将以中文显示和翻译') } } } return null } async function SetConfigMenu() {//初始化油猴菜单 var name = {} name.itme1 = getLang("原文与译文并行显示") + (config.bilingual ? " [√]" : " [×]") GM_registerMenuCommand(name.itme1, function () { GM_setValue("bilingual", !config.bilingual) config.bilingual = GM_getValue("bilingual", true) uninstallAll() SetConfigMenu() }) var size = parseInt(JSON.stringify(GM_getValue("local_data", {})).length / 1024) if (size > 2048) { uninstallAll() var size = parseInt(JSON.stringify(GM_getValue("local_data", {})).length / 1024) } name.itme2 = getLang("清理翻译缓存 ") + size + "kb" GM_registerMenuCommand(name.itme2, function () { ClearOffline() uninstallAll() SetConfigMenu() }) return null function uninstallAll() { for (key in name) { GM_unregisterMenuCommand(name[key]); } } } function Does_it_contain_ch(text) { var size = /[\u4e00-\u9fa5]/.exec(text) if (size == null) { return false //未知 } else { size = /[ぁ-んァ-ヶ]/.exec(text)//匹配片假名 if (size == null) { return true //中文 } else { return false //日语 } } } function Toast(msg) { var m = document.createElement('div'); m.innerHTML = msg; m.style.cssText = "z-index: 999;font-size: .32rem;color: rgb(255, 255, 255);background-color: rgba(0, 0, 0, 0.6);padding: 10px 15px;margin: 0 0 0 -60px;border-radius: 4px;position: fixed; top: 50%;left: 50%;width: 130px;text-align: center;"; document.body.appendChild(m); return m } function hashVal(string) { var hash = 0, i, chr; if (string.length === 0) return hash; for (i = 0; i < string.length; i++) { chr = string.charCodeAt(i); hash = ((hash << 5) - hash) + chr; hash |= 0; // Convert to 32bit integer } return hash; } function GM_getValue_local(id, key, defaultValue) { var data = GM_getValue(id, {}); return data[key] || defaultValue; } function GM_setValue_local(id, key, value) { var data = GM_getValue(id, {}); data[key] = value; return GM_setValue(id, data); } function ClearOffline() { var value = GM_getValue("local_data", {}) var newDice = {} var time = new Date() * 1 for (key in value) { var disparity = parseInt((time - value[key].time) / 1000) if (disparity < 432000) {//缓存5天 newDice[key] = value[key] } else { console.log("移除缓存", value[key]); } } GM_setValue("local_data", newDice) } }())